一覧に戻る

Satoriを用いて動的にOGPを生成する際のtips

#Node.js#font#OGP#TypeScript

TL;DR

  • SatoriによるSVG生成にはフォントファイルの読み込みが必要
  • フォントファイルの読み込みはモジュールバンドラーとの相性が良くない
  • vite-plugin-arraybufferを利用してインラインでフォントファイルを読み込むことですっきりビルド

動的OGP生成の手法

筆者所属のMIERUNEにて最近QGIS LABというWebサイトをリリースしました。

https://qgis.mierune.co.jp/

このサイトの記事ページでは、記事タイトルから動的にOGP画像を生成しています。

https://qgis.mierune.co.jp/posts/howto_1_download_ksj-data

動的OGPの生成は下記の手順がポピュラーのようです。

  1. vercel/satoriで、SVGを生成する(JSXによる記述が可能)
  2. SVGを画像化(Node.jsならlovell/sharp、それ以外だとyisibi/resvg-jsも利用できそうです)

これらの利用方法は以下のWebサイトが詳しいです。

https://azukiazusa.dev/blog/satori-sveltekit-ogp-image/

satoriにはフォントファイルが必要

satoriはSVG生成時の第二引数でオプションを設定するのですが、そのオプションにはフォントの指定が必須です。フォントデータはBuffer型である必要があります。サンプルコードによれば、fsfetchで読んできてねと書いてあります。

import satori from 'satori'

const svg = await satori(
  <div style={{ color: 'black' }}>hello, world</div>,
  {
    width: 600,
    height: 400,
    fonts: [
      {
        name: 'Roboto',
        // Use `fs` (Node.js only) or `fetch` to read the font as Buffer/ArrayBuffer and provide `data` here.
        data: robotoArrayBuffer,
        weight: 400,
        style: 'normal',
      },
    ],
  },
)

動的に作るので、外部からフォントファイルをダウンロードする時間も節約したいので、fetchを利用する線はなくなります。

モジュールバンドラーが介在する場合の問題

fsで静的なファイルを読み込むと、以下のようなコードになります。

const font = fs.readFileSync('./fonts/NotoSansJP-Regular.otf')

このときファイルパスを指定するのですが、このパスはスクリプト実行時点のカレントディレクトリから見た相対パス(あるいは絶対パス)となります。

今回のWebサイトはSvelteKitで構築されているため、ソースコードとビルドアーティファクトのディレクトリ構成は全く異なります(なので相対パスで書きづらい)。また、実行環境に依存したコードもあまり書きたくないので、絶対パスもなるべく利用したくありません。

解決策:ビルド時にファイルをインラインに読み込んでしまう

https://github.com/tachibana-shin/vite-plugin-arraybuffer

SvelteKitではビルドにViteを利用するので、Viteのプラグインを利用してフォントファイルをimportすることで、バイナリデータをインラインで持ってしまうという策です。これによりシンプルに解決できて、以下がサンプルコードです。

import NotoSansJpBold from './NotoSansJP-Bold.ttf?arraybuffer&base64';
// "?arraybuffer"というsuffixで、vite-plugin-arraybufferがファイルをバンドルしてくれる
// "&base64"により、base64文字列として読まれる(今回の実装では必要だった)

const svg = await satori(
    <div>...</div>,
    {
        width: 1200,
        height: 630,
        fonts: [
            {
                name: 'Noto Sans JP Bold',
                data: NotoSansJpBold
            }
        ]
    }
);

// svg to webp
const buffer = await sharp(Buffer.from(svg)).webp().toBuffer();
return buffer;

これをビルドすると以下のようなコードになります。base64文字列がインラインに埋め込まれていることがわかりますね。

const NotoSansJpBold = decode64("AAEAAAATAQAABAAw...")

まとめ

  • この手を試す前はsapphi-red/vite-plugin-static-copyを用いてビルドアーティファクトにフォントファイルをコピーするような手も試しました。この手でも頑張れば解決は出来ると思いますが、実行環境に依存したコードが増えてポータビリティが低下すると思います(例:今回で言うと、AWS AmplifyとAWS Lambdaで動く必要があった)
  • バンドラーにViteを利用しているなら、今回紹介したプラグインが有効なケースは結構あるかもしれません